// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.linqingying.gitcode;

import com.intellij.ide.BrowserUtil;
import com.intellij.notebook.editor.BackedVirtualFile;
import com.intellij.openapi.actionSystem.ActionUpdateThread;
import com.intellij.openapi.actionSystem.AnActionEvent;
import com.intellij.openapi.actionSystem.CommonDataKeys;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.ReadAction;
import com.intellij.openapi.application.WriteAction;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.fileEditor.FileDocumentManager;
import com.intellij.openapi.fileTypes.FileTypeManager;
import com.intellij.openapi.ide.CopyPasteManager;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.progress.Task;
import com.intellij.openapi.project.DumbAwareAction;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Condition;
import com.intellij.openapi.util.Ref;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vcs.changes.ChangeListManager;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.util.containers.ContainerUtil;
import com.linqingying.gitcode.api.GitCodeApiRequestExecutor;
import com.linqingying.gitcode.api.GitCodeApiRequests;
import com.linqingying.gitcode.api.GitCodeServerPath;
import com.linqingying.gitcode.api.data.request.GitCodeGistRequest.FileContent;
import com.linqingying.gitcode.authentication.accounts.GCAccountManager;
import com.linqingying.gitcode.authentication.accounts.GitCodeAccount;
import com.linqingying.gitcode.i18n.GitCodeBundle;
import com.linqingying.gitcode.icons.GitCodeIcons;
import com.linqingying.gitcode.ui.GitCodeCreateGistDialog;
import com.linqingying.gitcode.util.*;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.awt.datatransfer.StringSelection;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import static java.util.Objects.requireNonNull;

/**
 * @author oleg
 */
public class GitCodeCreateGistAction extends DumbAwareAction {
    private static final Logger LOG = GitCodeUtil.LOG;
    private static final Condition<@Nullable VirtualFile> FILE_WITH_CONTENT = f -> f != null && !(f.getFileType().isBinary());

    protected GitCodeCreateGistAction() {
        super(GitCodeBundle.messagePointer("create.gist.action.title"),
                GitCodeBundle.messagePointer("create.gist.action.description"),
                GitCodeIcons.GIT_CODE_ICON);
    }

    private static VirtualFile @Nullable [] filterFilesWithContent(@Nullable VirtualFile @Nullable [] files) {
        if (files == null) return null;

        return ContainerUtil.filter(files, FILE_WITH_CONTENT).toArray(VirtualFile.EMPTY_ARRAY);
    }

    private static void createGistAction(@NotNull final Project project,
                                         @Nullable final Editor editor,
                                         @Nullable final VirtualFile file,
                                         final VirtualFile @Nullable [] files) {
        GitCodeSettings settings = GitCodeSettings.getInstance();
        // Ask for description and other params
        GitCodeCreateGistDialog dialog = new GitCodeCreateGistDialog(project,
                getFileName(editor, files),
                settings.isPrivateGist(),
                settings.isOpenInBrowserGist(),
                settings.isCopyURLGist());
        if (!dialog.showAndGet()) {
            return;
        }
        settings.setPrivateGist(dialog.isSecret());
        settings.setOpenInBrowserGist(dialog.isOpenInBrowser());
        settings.setCopyURLGist(dialog.isCopyURL());

        GitCodeAccount account = requireNonNull(dialog.getAccount());

        final Ref<String> url = new Ref<>();
        new Task.Backgroundable(project, GitCodeBundle.message("create.gist.process")) {
            @Override
            public void run(@NotNull ProgressIndicator indicator) {
                String token = GCCompatibilityUtil.getOrRequestToken(account, project);
                if (token == null) return;
                GitCodeApiRequestExecutor requestExecutor = GitCodeApiRequestExecutor.Factory.getInstance().create(account.getServer(), token);

                List<FileContent> contents = collectContents(project, editor, file, files);
                if (contents.isEmpty()) return;

                String gistUrl = createGist(project, requestExecutor, indicator, account.getServer(),
                        contents, dialog.isSecret(), dialog.getDescription(), dialog.getFileName());
                url.set(gistUrl);
            }

            @Override
            public void onSuccess() {
                if (url.isNull()) {
                    return;
                }
                if (dialog.isCopyURL()) {
                    StringSelection stringSelection = new StringSelection(url.get());
                    CopyPasteManager.getInstance().setContents(stringSelection);
                }
                if (dialog.isOpenInBrowser()) {
                    BrowserUtil.browse(url.get());
                } else {
                    GitCodeNotifications
                            .showInfoURL(project,
                                    GitCodeNotificationIdsHolder.GIST_CREATED,
                                    GitCodeBundle.message("create.gist.success"),
                                    GitCodeBundle.message("create.gist.url"), url.get());
                }
            }
        }.queue();
    }

    @Nullable
    private static String getFileName(@Nullable Editor editor, VirtualFile @Nullable [] files) {
        if (files != null && files.length == 1 && !files[0].isDirectory()) {
            return files[0].getName();
        }
        if (editor != null) {
            return "";
        }
        return null;
    }

    @NotNull
    static List<FileContent> collectContents(@NotNull Project project,
                                             @Nullable Editor editor,
                                             @Nullable VirtualFile file,
                                             VirtualFile @Nullable [] files) {
        boolean isBackedFile = file != null && file instanceof BackedVirtualFile;
        if (editor != null) {
            String content = getContentFromEditor(editor, isBackedFile);
            if (content != null) {
                if (file != null) {
                    return Collections.singletonList(new FileContent(file.getName(), content));
                } else {
                    return Collections.singletonList(new FileContent("", content));
                }
            }
        }
        if (files != null) {
            List<FileContent> contents = new ArrayList<>();
            for (VirtualFile vf : files) {
                contents.addAll(getContentFromFile(vf, project, null));
            }
            return contents;
        }

        if (file != null) {
            return getContentFromFile(file, project, null);
        }

        LOG.error("File, files and editor can't be null all at once!");
        throw new IllegalStateException("File, files and editor can't be null all at once!");
    }

    @Nullable
    static String createGist(@NotNull Project project,
                             @NotNull GitCodeApiRequestExecutor executor,
                             @NotNull ProgressIndicator indicator,
                             @NotNull GitCodeServerPath server,
                             @NotNull List<? extends FileContent> contents,
                             final boolean isSecret,
                             @NotNull final String description,
                             @Nullable String filename) {
        if (contents.isEmpty()) {
            GitCodeNotifications.showWarning(project,
                    GitCodeNotificationIdsHolder.GIST_CANNOT_CREATE,
                    GitCodeBundle.message("cannot.create.gist"),
                    GitCodeBundle.message("create.gist.error.empty"));
            return null;
        }
        if (contents.size() == 1 && filename != null) {
            FileContent entry = contents.iterator().next();
            contents = Collections.singletonList(new FileContent(filename, entry.getContent()));
        }
        try {
            return executor.execute(indicator, GitCodeApiRequests.Gists.create(server, contents, description, !isSecret)).getHtmlUrl();
        } catch (IOException e) {
            GitCodeNotifications.showError(project,
                    GitCodeNotificationIdsHolder.GIST_CANNOT_CREATE,
                    GitCodeBundle.message("cannot.create.gist"),
                    e);
            return null;
        }
    }

    @Nullable
    private static String getContentFromEditor(@NotNull final Editor editor, boolean onlySelection) {
        String text = ReadAction.compute(() -> editor.getSelectionModel().getSelectedText());
        if (text == null && !onlySelection) {
            text = editor.getDocument().getText();
        }

        if (StringUtil.isEmptyOrSpaces(text)) {
            return null;
        }
        return text;
    }

    @NotNull
    private static List<FileContent> getContentFromFile(@NotNull final VirtualFile file, @NotNull Project project, @Nullable String prefix) {
        final VirtualFile realFile = BackedVirtualFile.getOriginFileIfBacked(file);
        if (realFile.isDirectory()) {
            return getContentFromDirectory(realFile, project, prefix);
        }
        if (realFile.getFileType().isBinary()) {
            GitCodeNotifications
                    .showWarning(project, GitCodeNotificationIdsHolder.GIST_CANNOT_CREATE,
                            GitCodeBundle.message("cannot.create.gist"),
                            GitCodeBundle.message("create.gist.error.binary.file", realFile.getName()));
            return Collections.emptyList();
        }
        String content = WriteAction.computeAndWait(() -> {
            try {
                FileDocumentManager fileDocumentManager = FileDocumentManager.getInstance();
                Document document = fileDocumentManager.getDocument(realFile);
                if (document != null) {
                    fileDocumentManager.saveDocument(document);
                    return document.getText();
                } else {
                    return new String(realFile.contentsToByteArray(), realFile.getCharset());
                }
            } catch (IOException e) {
                LOG.info("Couldn't read contents of the file " + realFile, e);
                return null;
            }
        });
        if (content == null) {
            GitCodeNotifications
                    .showWarning(project,
                            GitCodeNotificationIdsHolder.GIST_CANNOT_CREATE,
                            GitCodeBundle.message("cannot.create.gist"),
                            GitCodeBundle.message("create.gist.error.content.read", realFile.getName()));
            return Collections.emptyList();
        }
        if (StringUtil.isEmptyOrSpaces(content)) {
            return Collections.emptyList();
        }
        String filename = addPrefix(realFile.getName(), prefix, false);
        return Collections.singletonList(new FileContent(filename, content));
    }

    @NotNull
    private static List<FileContent> getContentFromDirectory(@NotNull VirtualFile dir, @NotNull Project project, @Nullable String prefix) {
        List<FileContent> contents = new ArrayList<>();
        for (VirtualFile file : dir.getChildren()) {
            if (!isFileIgnored(file, project)) {
                String pref = addPrefix(dir.getName(), prefix, true);
                contents.addAll(getContentFromFile(file, project, pref));
            }
        }
        return contents;
    }

    private static String addPrefix(@NotNull String name, @Nullable String prefix, boolean addTrailingSlash) {
        String pref = prefix == null ? "" : prefix;
        pref += name;
        if (addTrailingSlash) {
            pref += "_";
        }
        return pref;
    }

    private static boolean isFileIgnored(@NotNull VirtualFile file, @NotNull Project project) {
        ChangeListManager manager = ChangeListManager.getInstance(project);
        return manager.isIgnoredFile(file) || FileTypeManager.getInstance().isFileIgnored(file);
    }

    @Override
    public @NotNull ActionUpdateThread getActionUpdateThread() {
        return ActionUpdateThread.BGT;
    }

    @Override
    public void update(@NotNull final AnActionEvent e) {
        Project project = e.getData(CommonDataKeys.PROJECT);
        if (project == null || project.isDefault()) {
            e.getPresentation().setEnabledAndVisible(false);
            return;
        }

        GCAccountManager accountManager = ApplicationManager.getApplication().getService(GCAccountManager.class);
        GCHostedRepositoriesManager hostedRepositoriesManager = project.getService(GCHostedRepositoriesManager.class);
        if (hostedRepositoriesManager.getKnownRepositoriesState().getValue().isEmpty() &&
                accountManager.getAccountsState().getValue().isEmpty()) {
            e.getPresentation().setEnabledAndVisible(false);
            return;
        }

        Editor editor = e.getData(CommonDataKeys.EDITOR);
        VirtualFile file = e.getData(CommonDataKeys.VIRTUAL_FILE);
        VirtualFile[] files = e.getData(CommonDataKeys.VIRTUAL_FILE_ARRAY);
        boolean hasFilesWithContent = FILE_WITH_CONTENT.value(file) || (files != null && ContainerUtil.exists(files, FILE_WITH_CONTENT));

        if (!hasFilesWithContent || editor != null && editor.getDocument().getTextLength() == 0) {
            e.getPresentation().setEnabledAndVisible(false);
            return;
        }

        e.getPresentation().setEnabledAndVisible(true);
    }

    @Override
    public void actionPerformed(@NotNull final AnActionEvent e) {
        final Project project = e.getData(CommonDataKeys.PROJECT);
        if (project == null || project.isDefault()) {
            return;
        }

        final Editor editor = e.getData(CommonDataKeys.EDITOR);
        final VirtualFile file = e.getData(CommonDataKeys.VIRTUAL_FILE);
        final VirtualFile[] files = e.getData(CommonDataKeys.VIRTUAL_FILE_ARRAY);
        if (editor == null && file == null && files == null) {
            return;
        }

        createGistAction(project, editor, FILE_WITH_CONTENT.value(file) ? file : null, filterFilesWithContent(files));
    }
}
